/*
 * Copyright Miroslav Pokorny
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package lovely.gwt.jsonrpc.client;

import rocket.remoting.client.RpcException;
import rocket.remoting.client.support.rpc.RpcServiceClient;
import rocket.util.client.Checker;

import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
import com.google.gwt.http.client.RequestBuilder.Method;
import com.google.gwt.json.client.JSONException;
import com.google.gwt.json.client.JSONNull;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.json.client.JSONString;
import com.google.gwt.json.client.JSONValue;
import com.google.gwt.user.client.rpc.AsyncCallback;

/**
 * The general base class for all json service stubs. The json response is
 * deserialized using the json serialization api, and then the given
 * AsyncCallback is invoked.
 * 
 * @author Stefan Martin
 */
abstract public class JsonServiceMethodInvoker<R> implements RequestCallback {

	protected JsonServiceMethodInvoker() {
		super();
	}

	/**
	 * Factory method which creates a new RequestBuilder.
	 * 
	 * @return A new RequestBuilder
	 */
	protected RequestBuilder createRequestBuilder() {
		return new RequestBuilder(this.getRequestMethod(), this.buildUrl());
	}

	/**
	 * This method will be used to build a url for the request that is about to
	 * be submitted.
	 * 
	 * @return The final url given to the RequestBuilder
	 */
	protected String buildUrl() {
		return this.getUrl();
	}

	protected String info;

	public String getInfo() {
		return info;
	}

	public void setInfo(String info) {
		this.info = info;

	}
	/**
	 * Takes the response and converts the json payload to a java instance using
	 * {@link #deserializeJsonPayload(String)}
	 * 
	 * @param request
	 *            The request
	 * @param response
	 *            The response
	 */
	protected void onSuccessfulResponse(final Request request, final Response response) {
		final AsyncCallback<R> callback = this.getCallback();
		R object = null;
		boolean skipOnFailure = false;

		try {
			final String text = response.getText();
			final JSONValue jsonValue = JSONParser.parse(text);
			if (jsonValue.isNull() == null) {
				JSONObject jsonObj = jsonValue.isObject();
				if (jsonObj != null) {
					final JSONValue version = jsonObj.get("version");
					if (version == null) {
						throw new JSONException("\nerror:\tJsonRpc ERROR: version required");
					}
					final JSONString versionString = version.isString();
					if (!versionString.stringValue().equals("1.1")) {
						throw new JSONException("\nerror:\tJsonRpc ERROR: wrong version "+versionString.stringValue());
					}
					final JSONValue result = jsonObj.get("result");
					final JSONValue error = jsonObj.get("error");
					if (result != null && error != null) {
						throw new JSONException("\nerror:\tJsonRpc ERROR: both result and error are set.");
					}
					if (error != null) {
						throw new JSONException("\nerror:\t" + error.toString());
					}
					if (result.isNull() instanceof JSONNull) {
						object = null;
						//throw new JSONException("\nerror:\tJsonRpc ERROR: result required");
					} else {
						object = this.readObject(result);
					}
					// invoke success! after deserializing...
					//skipOnFailure = true;
					callback.onSuccess(object);
					
				} else {
					throw new JSONException("\nerror:\tJsonRpc ERROR: invalid response");
				}
			} else {
				throw new JSONException("\nerror:\tJsonRpc ERROR: no response");
			}

		} catch (final Throwable caught) {
			if (!skipOnFailure) {
				// invoke failure if anything went wrong.
				callback.onFailure(caught);
			}
		}
	}

	/**
	 * This method is implemented by the generator and acts a bridge calling GWT
	 * to create a Serializer which will deserialize the incoming JSON stream
	 * into a java object.
	 * 
	 * @param jsonValue
	 *            The response from the json service.
	 * @return The deserialized object
	 */
	abstract protected R readObject(final JSONValue jsonValue);

	/**
	 * Copies over the user credentials, timeout and service entry point from
	 * the rpc service client to this particular method invoker.
	 * 
	 * @param client
	 *            The source
	 */
	public void prepare(final RpcServiceClient client) {
		Checker.notNull("parameter:client", client);

		this.setUrl(client.getServiceEntryPoint());

		// copy over any authentication details
		if (client.hasUsername()) {
			this.setUsername(this.getUsername());
			this.setPassword(this.getPassword());
		}

		// copy timeout value if present...
		if (client.hasTimeout()) {
			this.setTimeout(this.getTimeout());
		}
	}

	/**
	 * Initiates a Http Request to the given url and provides a bridge between
	 * the {@link RequestCallback} and the given {@link AsyncCallback}.
	 */
	public void makeRequest() {
		final RequestBuilder request = this.createRequestBuilder();

		if (this.hasUsername()) {
			request.setUser(this.getUsername());
			request.setPassword(this.getPassword());
		}

		if (this.hasTimeout()) {
			request.setTimeoutMillis(this.getTimeout());
		}

		this.setHeaders(request);

		try {
			System.out.println("JSON-RPC sent:     " + this.getRequestData());
			request.sendRequest(this.getRequestData(), this);
		} catch (final RequestException requestException) {
			this.onFailedRequest(requestException);
		}
	}

	abstract JSONValue getParameters();

	String getRequestData() {
		final JSONObject json = new JSONObject();
        json.put("id", new JSONString("0"));
        json.put("version", new JSONString("1.1"));
        json.put("method",new JSONString(getMethodName()));
        json.put("params", this.getParameters());
        return json.toString();
    }

	/**
	 * This method returns the request method.
	 * 
	 * @return
	 */
	Method getRequestMethod() {
		return RequestBuilder.POST;
	}

	/**
	 * This method sets the header.
	 */
	protected void setHeaders(final RequestBuilder request) {
		request.setHeader(Constants.CONTENT_TYPE_HEADER, Constants.JSON_RPC_CONTENT_TYPE);
		request.setHeader(Constants.CONTENT_LENGTH_HEADER, "" + this.getRequestData().length());
	}

	public void onResponseReceived(final Request request, final Response response) {
		final int status = response.getStatusCode();

		System.out.println("JSON-RPC received: " + response.getText());

		if (status == Constants.HTTP_RESPONSE_OK) {
			this.onSuccessfulResponse(request, response);
		} else {
			this.onFailedResponse(request, response);
		}
	}

	/**
	 * Dispatches to one of two methods depending on the response code.
	 */
	public void onError(final Request request, final Throwable caught) {
		this.onFailedRequest(caught);
	}

	/**
	 * This method is invoked if a request fails for any reason that is not the
	 * result of a server failure.
	 * 
	 * @param throwable
	 *            A throwable holding what went wrong.
	 */
	protected void onFailedRequest(final Throwable throwable) {
		final Throwable wrapper = new RpcException("Call to server failed: " + throwable.getMessage(), throwable);
		this.getCallback().onFailure(wrapper);
	}

	/**
	 * Creates an exception that expresses the reason why the server invocation
	 * failed, and executes the {@link AsyncCallback#onFailure(Throwable)}
	 * 
	 * @param request
	 * @param response
	 */
	protected void onFailedResponse(final Request request, final Response response) {
		this.getCallback().onFailure(
				new RpcException("Call failed on server, " + response.getStatusText() + "(" + response.getStatusCode() + ")"));
	}

	/**
	 * This property will be set by copying the serviceEntryPoint property from
	 * the matching JsonRpcServiceClient.
	 */
	private String url;

	protected String getUrl() {
		Checker.notEmpty("field:url", url);
		return this.url;
	}

	public void setUrl(final String url) {
		Checker.notEmpty("parameter:url", url);
		this.url = url;
	}

	/**
	 * When present a username and password is also attached to the request.
	 */
	private String username;

	protected String getUsername() {
		Checker.notEmpty("field:username", username);
		return this.username;
	}

	public boolean hasUsername() {
		return null != username;
	}

	public void setUsername(final String username) {
		Checker.notEmpty("parameter:username", username);
		this.username = username;
	}

	/**
	 * To add authentication to any request both the username and password
	 * properties must be set.
	 */
	private String password;

	protected String getPassword() {
		Checker.notEmpty("field:password", password);
		return this.password;
	}

	public boolean hasPassword() {
		return null != password;
	}

	public void setPassword(final String password) {
		Checker.notEmpty("parameter:password", password);
		this.password = password;
	}

	/**
	 * This property must be set to allow a custom timeout value for this rpc.
	 */
	private int timeout;

	protected int getTimeout() {
		Checker.greaterThan("field:timeout", 0, timeout);
		return timeout;
	}

	public boolean hasTimeout() {
		return this.timeout > 0;
	}

	public void setTimeout(final int timeout) {
		Checker.greaterThan("parameter:timeout", 0, timeout);
		this.timeout = timeout;
	}

	/**
	 * The parameter that will be serialized and sent to the json rpc service on
	 * the server.
	 */
	private String methodName;

	public String getMethodName() {
		Checker.notNull("field:methodName", methodName);
		return this.methodName;
	}

	public void setMethodName(final String methodName) {
		Checker.notNull("parameter:methodName", methodName);
		this.methodName = methodName;
	}

	/**
	 * The request parameterType being executed
	 */
	private String requestParameterType;

	protected String getRequestParameterType() {
		Checker.notNull("field:requestParameterType", requestParameterType);
		return this.requestParameterType;
	}

	public void setRequestParameterType(final String requestParameterType) {
		Checker.notNull("parameter:requestParameterType", requestParameterType);
		this.requestParameterType = requestParameterType;
	}

	/**
	 * The callback that will have either of its two method invoked depending on
	 * the result recieved from the server.
	 */
	private AsyncCallback<R> callback;

	AsyncCallback<R> getCallback() {
		Checker.notNull("field:callback", callback);
		return this.callback;
	}

	public void setCallback(final AsyncCallback<R> callback) {
		Checker.notNull("parameter:callback", callback);
		this.callback = callback;
	}
	
}
